SHEETS RAG
工作流概述
这是一个包含23个节点的复杂工作流,主要用于自动化处理各种任务。
工作流源代码
{
"id": "7gRbzEzCuOzQKn4M",
"meta": {
"instanceId": "edc0464b1050024ebda3e16fceea795e4fdf67b1f61187c4f2f3a72397278df0",
"templateCredsSetupCompleted": true
},
"name": "SHEETS RAG",
"tags": [],
"nodes": [
{
"id": "a073154f-53ad-45e2-9937-d0a4196c7838",
"name": "create table query",
"type": "n8n-nodes-base.code",
"position": [
1280,
2360
],
"parameters": {
"jsCode": "// Helper function to check if a string is in MM/DD/YYYY format
function isDateString(value) {
const dateRegex = /^\d{2}\/\d{2}\/\d{4}$/;
if (typeof value !== 'string') return false;
if (!dateRegex.test(value)) return false;
const [month, day, year] = value.split('/').map(Number);
const date = new Date(year, month - 1, day);
return !isNaN(date.getTime());
}
const tableName = `ai_table_${$('change_this').first().json.sheet_name}`;
const rows = $('fetch sheet data').all();
const allColumns = new Set();
// Collect column names dynamically
rows.forEach(row => {
Object.keys(row.json).forEach(col => allColumns.add(col));
});
// Ensure \"ai_table_identifier\" is always the first column
const originalColumns = [\"ai_table_identifier\", ...Array.from(allColumns)];
// Function to detect currency type (unchanged)
function detectCurrency(values) {
const currencySymbols = {
'₹': 'INR', '$': 'USD', '€': 'EUR', '£': 'GBP', '¥': 'JPY',
'₩': 'KRW', '฿': 'THB', 'zł': 'PLN', 'kr': 'SEK', 'R$': 'BRL',
'C$': 'CAD', 'A$': 'AUD'
};
let detectedCurrency = null;
for (const value of values) {
if (typeof value === 'string' && value.trim() !== '') {
for (const [symbol, code] of Object.entries(currencySymbols)) {
if (value.trim().startsWith(symbol)) {
detectedCurrency = code;
break;
}
}
}
}
return detectedCurrency;
}
// Function to generate consistent column names
function generateColumnName(originalName, typeInfo) {
if (typeInfo.isCurrency) {
return `${originalName}_${typeInfo.currencyCode.toLowerCase()}`;
}
return originalName;
}
// Infer column types and transform names
const columnMapping = {};
originalColumns.forEach(col => {
let typeInfo = { type: \"TEXT\" };
if (col !== \"ai_table_identifier\") {
const sampleValues = rows
.map(row => row.json[col])
.filter(value => value !== undefined && value !== null);
// Check for currency first
const currencyCode = detectCurrency(sampleValues);
if (currencyCode) {
typeInfo = { type: \"DECIMAL(15,2)\", isCurrency: true, currencyCode };
}
// If all sample values match MM/DD/YYYY, treat the column as a date
else if (sampleValues.length > 0 && sampleValues.every(val => isDateString(val))) {
typeInfo = { type: \"TIMESTAMP\" };
}
}
const newColumnName = generateColumnName(col, typeInfo);
columnMapping[col] = { newName: newColumnName, typeInfo };
});
// Final column names
const mappedColumns = originalColumns.map(col => columnMapping[col]?.newName || col);
// Define SQL columns – note that for simplicity, this example still uses TEXT for non-special types,
// but you can adjust it so that TIMESTAMP columns are created with a TIMESTAMP type.
const columnDefinitions = [`\"ai_table_identifier\" UUID PRIMARY KEY DEFAULT gen_random_uuid()`]
.concat(mappedColumns.slice(1).map(col => {
// If the column was inferred as TIMESTAMP, use that type in the CREATE TABLE statement.
const originalCol = Object.keys(columnMapping).find(key => columnMapping[key].newName === col);
const inferredType = columnMapping[originalCol]?.typeInfo?.type;
return `\"${col}\" ${inferredType === \"TIMESTAMP\" ? \"TIMESTAMP\" : \"TEXT\"}`;
}))
.join(\", \");
const createTableQuery = `CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefinitions});`;
return [{
query: createTableQuery,
columnMapping: columnMapping
}];
"
},
"typeVersion": 2
},
{
"id": "2beb72c4-dab4-4058-b587-545a8ce8b86d",
"name": "create insertion query",
"type": "n8n-nodes-base.code",
"position": [
1660,
2360
],
"parameters": {
"jsCode": "const tableName = `ai_table_${$('change_this').first().json.sheet_name}`;
const rows = $('fetch sheet data').all();
const allColumns = new Set();
// Get column mapping from previous node
const columnMapping = $('create table query').first().json.columnMapping || {};
// Collect column names dynamically
rows.forEach(row => {
Object.keys(row.json).forEach(col => allColumns.add(col));
});
const originalColumns = Array.from(allColumns);
const mappedColumns = originalColumns.map(col =>
columnMapping[col] ? columnMapping[col].newName : col
);
// Helper function to check if a string is a valid timestamp
function isValidTimestamp(value) {
const date = new Date(value);
return !isNaN(date.getTime());
}
// Helper to detect currency symbol (unchanged)
function getCurrencySymbol(value) {
if (typeof value !== 'string') return null;
const currencySymbols = ['₹', '$', '€', '£', '¥', '₩', '฿', 'zł', 'kr', 'R$', 'C$', 'A$'];
for (const symbol of currencySymbols) {
if (value.trim().startsWith(symbol)) {
return symbol;
}
}
return null;
}
// Helper to normalize currency values (unchanged)
function normalizeCurrencyValue(value, currencySymbol) {
if (typeof value !== 'string') return null;
if (!currencySymbol) return value;
const numericPart = value.replace(currencySymbol, '').replace(/,/g, '');
return !isNaN(parseFloat(numericPart)) ? parseFloat(numericPart) : null;
}
// Helper to normalize percentage values (unchanged)
function normalizePercentageValue(value) {
if (typeof value !== 'string') return value;
if (!value.trim().endsWith('%')) return value;
const numericPart = value.replace('%', '');
return !isNaN(parseFloat(numericPart)) ? parseFloat(numericPart) / 100 : null;
}
// Function to parse MM/DD/YYYY strings into ISO format
function parseDateString(value) {
const dateRegex = /^\d{2}\/\d{2}\/\d{4}$/;
if (typeof value === 'string' && dateRegex.test(value)) {
const [month, day, year] = value.split('/').map(Number);
const date = new Date(year, month - 1, day);
return !isNaN(date.getTime()) ? date.toISOString() : null;
}
return value;
}
// Format rows properly based on column mappings and types
const formattedRows = rows.map(row => {
const formattedRow = {};
originalColumns.forEach((col, index) => {
const mappedCol = mappedColumns[index];
let value = row.json[col];
const typeInfo = columnMapping[col]?.typeInfo || { type: \"TEXT\" };
if (value === \"\" || value === null || value === undefined) {
value = null;
}
else if (typeInfo.isCurrency) {
const symbol = getCurrencySymbol(value);
if (symbol) {
value = normalizeCurrencyValue(value, symbol);
} else {
value = null;
}
}
else if (typeInfo.isPercentage) {
if (typeof value === 'string' && value.trim().endsWith('%')) {
value = normalizePercentageValue(value);
} else {
value = !isNaN(parseFloat(value)) ? parseFloat(value) / 100 : null;
}
}
else if (typeInfo.type === \"DECIMAL(15,2)\" || typeInfo.type === \"INTEGER\") {
if (typeof value === 'string') {
const cleanedValue = value.replace(/,/g, '');
value = !isNaN(parseFloat(cleanedValue)) ? parseFloat(cleanedValue) : null;
} else if (typeof value === 'number') {
value = parseFloat(value);
} else {
value = null;
}
}
else if (typeInfo.type === \"BOOLEAN\") {
if (typeof value === 'string') {
const lowercased = value.toString().toLowerCase();
value = lowercased === \"true\" ? true :
lowercased === \"false\" ? false : null;
} else {
value = Boolean(value);
}
}
else if (typeInfo.type === \"TIMESTAMP\") {
// Check if the value is in MM/DD/YYYY format and parse it accordingly.
if (/^\d{2}\/\d{2}\/\d{4}$/.test(value)) {
value = parseDateString(value);
} else if (isValidTimestamp(value)) {
value = new Date(value).toISOString();
} else {
value = null;
}
}
else if (typeInfo.type === \"TEXT\") {
value = value !== null && value !== undefined ? String(value) : null;
}
formattedRow[mappedCol] = value;
});
return formattedRow;
});
// Generate SQL placeholders dynamically
const valuePlaceholders = formattedRows.map((_, rowIndex) =>
`(${mappedColumns.map((_, colIndex) => `$${rowIndex * mappedColumns.length + colIndex + 1}`).join(\", \")})`
).join(\", \");
// Build the insert query string
const insertQuery = `INSERT INTO ${tableName} (${mappedColumns.map(col => `\"${col}\"`).join(\", \")}) VALUES ${valuePlaceholders};`;
// Flatten parameter values for PostgreSQL query
const parameters = formattedRows.flatMap(row => mappedColumns.map(col => row[col]));
return [
{
query: insertQuery,
parameters: parameters
}
];
"
},
"typeVersion": 2
},
{
"id": "ba19c350-ffb7-4fe1-9568-2a619c914434",
"name": "Google Drive Trigger",
"type": "n8n-nodes-base.googleDriveTrigger",
"position": [
600,
2060
],
"parameters": {
"pollTimes": {
"item": [
{}
]
},
"triggerOn": "specificFile",
"fileToWatch": {
"__rl": true,
"mode": "list",
"value": "1yGx4ODHYYtPV1WZFAtPcyxGT2brcXM6pl0KJhIM1f_c",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1yGx4ODHYYtPV1WZFAtPcyxGT2brcXM6pl0KJhIM1f_c/edit?usp=drivesdk",
"cachedResultName": "Spreadsheet"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "zOt0lyWOZz1UlS67",
"name": "Google Drive account"
}
},
"typeVersion": 1
},
{
"id": "dd2108fe-0cfe-453c-ac03-c0c5b10397e6",
"name": "execute_query_tool",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"position": [
1340,
1720
],
"parameters": {
"name": "query_executer",
"schemaType": "manual",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "oPWJZynrMME45ks4",
"cachedResultName": "query_executer"
},
"description": "Call this tool to execute a query. Remember that it should be in a postgreSQL query structure.",
"inputSchema": "{
\"type\": \"object\",
\"properties\": {
\"sql\": {
\"type\": \"string\",
\"description\": \"A SQL query based on the users question and database schema.\"
}
}
}",
"specifyInputSchema": true
},
"typeVersion": 1.2
},
{
"id": "f2c110db-1097-4b96-830d-f028e08b6713",
"name": "Google Gemini Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
880,
1680
],
"parameters": {
"options": {},
"modelName": "models/gemini-2.0-flash"
},
"credentials": {
"googlePalmApi": {
"id": "Kr5lNqvdmtB0Ybyo",
"name": "Google Gemini(PaLM) Api account"
}
},
"typeVersion": 1
},
{
"id": "2460801c-5b64-41b3-93f7-4f2fbffabfd6",
"name": "get_postgres_schema",
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"position": [
1160,
1720
],
"parameters": {
"name": "get_postgres_schema",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "iNLPk34SeRGHaeMD",
"cachedResultName": "get database schema"
},
"description": "Call this tool to retrieve the schema of all the tables inside of the database. A string will be retrieved with the name of the table and its columns, each table is separated by \n\n.",
"workflowInputs": {
"value": {},
"schema": [],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"typeVersion": 2
},
{
"id": "4b43ff94-df0d-40f1-9f51-cf488e33ff68",
"name": "change_this",
"type": "n8n-nodes-base.set",
"position": [
800,
2060
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "908ed843-f848-4290-9cdb-f195d2189d7c",
"name": "table_url",
"type": "string",
"value": "https://docs.google.com/spreadsheets/d/1yGx4ODHYYtPV1WZFAtPcyxGT2brcXM6pl0KJhIM1f_c/edit?gid=0#gid=0"
},
{
"id": "50f8afaf-0a6c-43ee-9157-79408fe3617a",
"name": "sheet_name",
"type": "string",
"value": "product_list"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "a27a47ff-9328-4eef-99e8-280452cff189",
"name": "is not in database",
"type": "n8n-nodes-base.if",
"position": [
1380,
2060
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "619ce84c-0a50-4f88-8e55-0ce529aea1fc",
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
},
"leftValue": "={{ $('table exists?').item.json.exists }}",
"rightValue": "true"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "8ad9bc36-08b1-408e-ba20-5618a801b4ed",
"name": "table exists?",
"type": "n8n-nodes-base.postgres",
"position": [
1000,
2060
],
"parameters": {
"query": "SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'ai_table_{{ $json.sheet_name }}'
);
",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"id": "KQiQIZTArTBSNJH7",
"name": "Postgres account"
}
},
"typeVersion": 2.5
},
{
"id": "f66b7ca7-ecb7-47fc-9214-2d2b37b0fbe4",
"name": "fetch sheet data",
"type": "n8n-nodes-base.googleSheets",
"position": [
1180,
2060
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('change_this').item.json.sheet_name }}"
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "={{ $('change_this').item.json.table_url }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "3au0rUsZErkG0zc2",
"name": "Google Sheets account"
}
},
"typeVersion": 4.5
},
{
"id": "11ba5da0-e7c4-49ee-8d35-24c8d3b9fea9",
"name": "remove table",
"type": "n8n-nodes-base.postgres",
"position": [
980,
2360
],
"parameters": {
"query": "DROP TABLE IF EXISTS ai_table_{{ $('change_this').item.json.sheet_name }} CASCADE;",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"id": "KQiQIZTArTBSNJH7",
"name": "Postgres account"
}
},
"typeVersion": 2.5
},
{
"id": "3936ecb3-f084-4f86-bd5f-abab0957ebc0",
"name": "create table",
"type": "n8n-nodes-base.postgres",
"position": [
1460,
2360
],
"parameters": {
"query": "{{ $json.query }}",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"id": "KQiQIZTArTBSNJH7",
"name": "Postgres account"
}
},
"typeVersion": 2.5
},
{
"id": "8a3ea239-f3fa-4c72-af99-31f4bd992b58",
"name": "perform insertion",
"type": "n8n-nodes-base.postgres",
"position": [
1860,
2360
],
"parameters": {
"query": "{{$json.query}}",
"options": {
"queryReplacement": "={{$json.parameters}}"
},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"id": "KQiQIZTArTBSNJH7",
"name": "Postgres account"
}
},
"typeVersion": 2.5
},
{
"id": "21239928-b573-4753-a7ca-5a9c3aa8aa3e",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
1720,
1720
],
"parameters": {},
"typeVersion": 1
},
{
"id": "c94256a9-e44e-4800-82f8-90f85ba90bde",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1920,
1460
],
"parameters": {
"color": 7,
"width": 500,
"height": 260,
"content": "Place this in a separate workflow named:
### query_executer"
},
"typeVersion": 1
},
{
"id": "daec928e-58ee-43da-bd91-ba8bcd639a4a",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1920,
1840
],
"parameters": {
"color": 7,
"width": 500,
"height": 280,
"content": "place this in a separate workflow named:
### get database schema"
},
"typeVersion": 1
},
{
"id": "8908e342-fcbe-4820-b623-cb95a55ea5db",
"name": "When chat message received",
"type": "@n8n/n8n-nodes-langchain.manualChatTrigger",
"position": [
640,
1540
],
"parameters": {},
"typeVersion": 1.1
},
{
"id": "d0ae90c2-169e-44d7-b3c2-4aff8e7d4be9",
"name": "response output",
"type": "n8n-nodes-base.set",
"position": [
2220,
1540
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e2f94fb1-3deb-466a-a36c-e3476511d5f2",
"name": "response",
"type": "string",
"value": "={{ $json }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "81c58d9b-ded4-4b74-8227-849e665cbdff",
"name": "sql query executor",
"type": "n8n-nodes-base.postgres",
"position": [
2000,
1540
],
"parameters": {
"query": "{{ $json.query.sql }}",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"id": "KQiQIZTArTBSNJH7",
"name": "Postgres account"
}
},
"typeVersion": 2.5
},
{
"id": "377d1727-4577-41bb-8656-38273fc4412b",
"name": "schema finder",
"type": "n8n-nodes-base.postgres",
"position": [
2000,
1920
],
"parameters": {
"query": "SELECT
t.schemaname,
t.tablename,
c.column_name,
c.data_type
FROM
pg_catalog.pg_tables t
JOIN
information_schema.columns c
ON t.schemaname = c.table_schema
AND t.tablename = c.table_name
WHERE
t.schemaname = 'public'
ORDER BY
t.tablename, c.ordinal_position;",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"id": "KQiQIZTArTBSNJH7",
"name": "Postgres account"
}
},
"typeVersion": 2.5
},
{
"id": "89d3c59c-2b67-454d-a8f3-e90e75a28a8c",
"name": "schema to string",
"type": "n8n-nodes-base.code",
"position": [
2220,
1920
],
"parameters": {
"jsCode": "function transformSchema(input) {
const tables = {};
input.forEach(({ json }) => {
if (!json) return;
const { tablename, schemaname, column_name, data_type } = json;
if (!tables[tablename]) {
tables[tablename] = { schema: schemaname, columns: [] };
}
tables[tablename].columns.push(`${column_name} (${data_type})`);
});
return Object.entries(tables)
.map(([tablename, { schema, columns }]) => `Table ${tablename} (Schema: ${schema}) has columns: ${columns.join(\", \")}`)
.join(\"\n\n\");
}
// Example usage
const input = $input.all();
const transformedSchema = transformSchema(input);
return { data: transformedSchema };"
},
"typeVersion": 2
},
{
"id": "42d1b316-60ca-49db-959b-581b162ca1f9",
"name": "AI Agent With SQL Query Prompt",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
900,
1540
],
"parameters": {
"options": {
"maxIterations": 5,
"systemMessage": "=## Role
You are a **Database Query Assistant** specializing in generating PostgreSQL queries based on natural language questions. You analyze database schemas, construct appropriate SQL queries, and provide clear explanations of results.
## Tools
1. `get_postgres_schema`: Retrieves the complete database schema (tables and columns)
2. `execute_query_tool`: Executes SQL queries with the following input format:
```json
{
\"sql\": \"Your SQL query here\"
}
```
## Process Flow
### 1. Analyze the Question
- Identify the **data entities** being requested (products, customers, orders, etc.)
- Determine the **query type** (COUNT, AVG, SUM, SELECT, etc.)
- Extract any **filters** or **conditions** mentioned
### 2. Fetch and Analyze Schema
- Call `get_postgres_schema` to retrieve database structure
- Identify relevant tables and columns that match the entities in the question
- Prioritize exact matches, then semantic matches
### 3. Query Construction
- Build case-insensitive queries using `LOWER(column) LIKE LOWER('%value%')`
- Filter out NULL or empty values with appropriate WHERE clauses
- Use joins when information spans multiple tables
- Apply aggregations (COUNT, SUM, AVG) as needed
### 4. Query Execution
- Execute query using the `execute_query_tool` with proper formatting
- If results require further processing, perform calculations as needed
### 5. Result Presentation
- Format results in a conversational, easy-to-understand manner
- Explain how the data was retrieved and any calculations performed
- When appropriate, suggest further questions the user might want to ask
## Best Practices
- Use parameterized queries to prevent SQL injection
- Implement proper error handling
- Respond with \"NOT_ENOUGH_INFO\" when the question lacks specificity
- Always verify table/column existence before attempting queries
- Use explicit JOINs instead of implicit joins
- Limit large result sets when appropriate
## Numeric Validation (IMPORTANT)
When validating or filtering numeric values in string columns:
1. **AVOID** complex regular expressions with `~` operator as they cause syntax errors
2. Use these safer alternatives instead:
```sql
-- Simple numeric check without regex
WHERE column_name IS NOT NULL AND trim(column_name) != '' AND column_name NOT LIKE '%[^0-9.]%'
-- For type casting with validation
WHERE column_name IS NOT NULL AND trim(column_name) != '' AND column_name ~ '[0-9]'
-- Safe numeric conversion
WHERE CASE WHEN column_name ~ '[0-9]' THEN TRUE ELSE FALSE END
```
3. For simple pattern matching, use LIKE instead of regex when possible
4. When CAST is needed, always guard against invalid values:
```sql
SELECT SUM(CASE WHEN column_name ~ '[0-9]' THEN CAST(column_name AS NUMERIC) ELSE 0 END) AS total
```
## Response Structure
1. **Analysis**: Brief mention of how you understood the question
2. **Query**: The SQL statement used (in code block format)
3. **Results**: Clear presentation of the data found
4. **Explanation**: Simple description of how the data was retrieved
## Examples
### Example 1: Basic Counting Query
**Question**: \"How many products are in the inventory?\"
**Process**:
1. Analyze schema to find product/inventory tables
2. Construct a COUNT query on the relevant table
3. Execute the query
4. Present the count with context
**SQL**:
```sql
SELECT COUNT(*) AS product_count
FROM products
WHERE quantity IS NOT NULL;
```
**Response**:
\"There are 1,250 products currently in the inventory. This count includes all items with a non-null quantity value in the products table.\"
### Example 2: Filtered Aggregation Query
**Question**: \"What is the average order value for premium customers?\"
**Process**:
1. Identify relevant tables (orders, customers)
2. Determine join conditions
3. Apply filters for \"premium\" customers
4. Calculate average
**SQL**:
```sql
SELECT AVG(o.total_amount) AS avg_order_value
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE LOWER(c.customer_type) = LOWER('premium')
AND o.total_amount IS NOT NULL;
```
**Response**:
\"Premium customers spend an average of $85.42 per order. This was calculated by averaging the total_amount from all orders placed by customers with a 'premium' customer type.\"
### Example 3: Numeric Calculation from String Column
**Question**: \"What is the total of all ratings?\"
**Process**:
1. Find the ratings table and column
2. Use safe numeric validation
3. Sum the values
**SQL**:
```sql
SELECT SUM(CASE WHEN rating ~ '[0-9]' THEN CAST(rating AS NUMERIC) ELSE 0 END) AS total_rating
FROM ratings
WHERE rating IS NOT NULL AND trim(rating) != '';
```
**Response**:
\"The sum of all ratings is 4,285. This calculation includes all valid numeric ratings from the ratings table.\"
### Example 4: Date Range Aggregation for Revenue
**Question**: \"How much did I make last week?\"
**Process**:
1. Identify the sales table and relevant columns (e.g., `sale_date` for dates and `revenue_amount` for revenue).
2. Use PostgreSQL date functions (`date_trunc` and interval arithmetic) to calculate the date range for the previous week.
3. Sum the revenue within the computed date range.
**SQL**:
```sql
SELECT SUM(revenue_amount) AS total_revenue
FROM sales_data
WHERE sale_date >= date_trunc('week', CURRENT_DATE) - INTERVAL '1 week'
AND sale_date < date_trunc('week', CURRENT_DATE);
```
**Response**:
\"Last week's total revenue is calculated by summing the `revenue_amount` for records where the `sale_date` falls within the previous week. This query uses date functions to dynamically determine the correct date range.\"
Today's date: {{ $now }}"
}
},
"typeVersion": 1.7
},
{
"id": "368d68d0-1fe0-4dbf-9b24-ac28fd6e74c3",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
1420
],
"parameters": {
"color": 6,
"width": 960,
"height": 460,
"content": "## Use a powerful LLM to correctly build the SQL queries, which will be identified from the get schema tool and then executed by the execute query tool."
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "d8045db4-2852-4bbe-9b97-0d3c0acb53f7",
"connections": {
"change_this": {
"main": [
[
{
"node": "table exists?",
"type": "main",
"index": 0
}
]
]
},
"create table": {
"main": [
[
{
"node": "create insertion query",
"type": "main",
"index": 0
}
]
]
},
"remove table": {
"main": [
[
{
"node": "create table query",
"type": "main",
"index": 0
}
]
]
},
"schema finder": {
"main": [
[
{
"node": "schema to string",
"type": "main",
"index": 0
}
]
]
},
"table exists?": {
"main": [
[
{
"node": "fetch sheet data",
"type": "main",
"index": 0
}
]
]
},
"fetch sheet data": {
"main": [
[
{
"node": "is not in database",
"type": "main",
"index": 0
}
]
]
},
"create table query": {
"main": [
[
{
"node": "create table",
"type": "main",
"index": 0
}
]
]
},
"execute_query_tool": {
"ai_tool": [
[
{
"node": "AI Agent With SQL Query Prompt",
"type": "ai_tool",
"index": 0
}
]
]
},
"is not in database": {
"main": [
[
{
"node": "create table query",
"type": "main",
"index": 0
}
],
[
{
"node": "remove table",
"type": "main",
"index": 0
}
]
]
},
"sql query executor": {
"main": [
[
{
"node": "response output",
"type": "main",
"index": 0
}
]
]
},
"get_postgres_schema": {
"ai_tool": [
[
{
"node": "AI Agent With SQL Query Prompt",
"type": "ai_tool",
"index": 0
}
]
]
},
"Google Drive Trigger": {
"main": [
[
{
"node": "change_this",
"type": "main",
"index": 0
}
]
]
},
"create insertion query": {
"main": [
[
{
"node": "perform insertion",
"type": "main",
"index": 0
}
]
]
},
"Execute Workflow Trigger": {
"main": [
[
{
"node": "sql query executor",
"type": "main",
"index": 0
},
{
"node": "schema finder",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent With SQL Query Prompt",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"When chat message received": {
"main": [
[
{
"node": "AI Agent With SQL Query Prompt",
"type": "main",
"index": 0
}
]
]
}
}
}
功能特点
- 自动检测新邮件
- AI智能内容分析
- 自定义分类规则
- 批量处理能力
- 详细的处理日志
技术分析
节点类型及作用
- Code
- Googledrivetrigger
- @N8N/N8N Nodes Langchain.Toolworkflow
- @N8N/N8N Nodes Langchain.Lmchatgooglegemini
- Set
复杂度评估
配置难度:
维护难度:
扩展性:
实施指南
前置条件
- 有效的Gmail账户
- n8n平台访问权限
- Google API凭证
- AI分类服务订阅
配置步骤
- 在n8n中导入工作流JSON文件
- 配置Gmail节点的认证信息
- 设置AI分类器的API密钥
- 自定义分类规则和标签映射
- 测试工作流执行
- 配置定时触发器(可选)
关键参数
| 参数名称 | 默认值 | 说明 |
|---|---|---|
| maxEmails | 50 | 单次处理的最大邮件数量 |
| confidenceThreshold | 0.8 | 分类置信度阈值 |
| autoLabel | true | 是否自动添加标签 |
最佳实践
优化建议
- 定期更新AI分类模型以提高准确性
- 根据邮件量调整处理批次大小
- 设置合理的分类置信度阈值
- 定期清理过期的分类规则
安全注意事项
- 妥善保管API密钥和认证信息
- 限制工作流的访问权限
- 定期审查处理日志
- 启用双因素认证保护Gmail账户
性能优化
- 使用增量处理减少重复工作
- 缓存频繁访问的数据
- 并行处理多个邮件分类任务
- 监控系统资源使用情况
故障排除
常见问题
邮件未被正确分类
检查AI分类器的置信度阈值设置,适当降低阈值或更新训练数据。
Gmail认证失败
确认Google API凭证有效且具有正确的权限范围,重新进行OAuth授权。
调试技巧
- 启用详细日志记录查看每个步骤的执行情况
- 使用测试邮件验证分类逻辑
- 检查网络连接和API服务状态
- 逐步执行工作流定位问题节点
错误处理
工作流包含以下错误处理机制:
- 网络超时自动重试(最多3次)
- API错误记录和告警
- 处理失败邮件的隔离机制
- 异常情况下的回滚操作